/*
 * The MIT License
 *
 * Copyright 2012 http://generic-toolset.googlecode.com.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.rlpw.swing;

import java.awt.Color;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import javax.swing.DefaultCellEditor;
import javax.swing.JTable;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.table.*;
import org.rlpw.swing.table.*;
import org.rlpw.swing.tree.Children;
import org.rlpw.swing.tree.Parent;

/**
 *
 * @param <T> the type of the elements
 */
public class JOTable<T> extends JTable implements View<T>, Selectable<T> {

    private List<ReflectionData> reflectionDataList;
    private Model<T> model;
    private FilteredList<T> flist;
    private ResourceBundle res;

    public JOTable(Class<T> c) {
        this(c, new DefaultModel<T>(), null);
    }

    public JOTable(Class<T> c, Model<T> m) {
        this(c, m, null);
    }

    public JOTable(Class<T> c, ResourceBundle res) {
        this(c, new DefaultModel<T>(), res);
    }

    public JOTable(Class<T> c, Model<T> m, ResourceBundle r) {

        res = r;
        reflectionDataList = new ReflectionDataList(c);
        flist = new FilteredList<>(this);
        initModel(m);
        for (ReflectionData ref : reflectionDataList) {

            Column cn = ref.getAnnotation();

            TableColumn tc = getColumnModel().getColumn(reflectionDataList.indexOf(ref));
            tc.setIdentifier(ref.getName());

            if (cn.width() > -1) {
                tc.setWidth(cn.width());
                tc.setPreferredWidth(cn.width());
            }
            if (!cn.resizable()) {
                tc.setMinWidth(tc.getWidth());
                tc.setMaxWidth(tc.getWidth());
            }
        }
        registerEditors();
        registerRenderers();

    }

    private void registerEditors() {
        setDefaultEditor(Date.class, new DateEditor());
        setDefaultEditor(Date.class, "net.sf.nachocalendar.table.DateFieldTableEditor");
        setDefaultEditor(Date.class, "com.toedter.calendar.JDateChooserCellEditor");
        setDefaultEditor(Date.class, "org.jdesktop.swingx.table.DatePickerCellEditor");
        setDefaultEditor(Color.class, new ColorEditor());
    }

    private void registerRenderers() {
        setDefaultRenderer(Date.class, new DateCellRenderer());
        setDefaultRenderer(Color.class, new ColorRenderer());
        setDefaultRenderer(Boolean.class, new BooleanRenderer());
    }

    /**
     * Sets a default cell editor by specified class name.
     *
     * @param columnClass set the default cell editor for this columnClass
     * @param editorClassName default cell editor class name to be used for this
     * columnClass
     * @see javax.swing.JTable#setDefaultEditor
     */
    public void setDefaultEditor(Class<?> columnClass, String editorClassName) {
        try {
            setDefaultEditor(columnClass, (TableCellEditor) Class.forName(editorClassName).newInstance());
        } catch (ReflectiveOperationException excep) {
        }
    }

    /**
     * Sets a default cell renderer by specified class name.
     *
     * @param columnClass set the default cell renderer for this columnClass
     * @param rendererClassName default cell renderer class name to be used for
     * this columnClass
     * @see javax.swing.JTable#setDefaultRenderer
     */
    public void setDefaultRenderer(Class<?> columnClass, String rendererClassName) {
        try {
            setDefaultRenderer(columnClass, (TableCellRenderer) Class.forName(rendererClassName).newInstance());
        } catch (ReflectiveOperationException excep) {
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public TableCellEditor getDefaultEditor(Class<?> columnClass) {
        if (!defaultEditorsByColumnClass.containsKey(columnClass) && columnClass.isEnum()) {
            return new DefaultCellEditor(new JEnumSelectBox(columnClass));
        } else {
            return super.getDefaultEditor(columnClass);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void updateView() {
        if (flist != null) {
            flist.update();
        }

        revalidate();
        repaint();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Model<T> getDataModel() {
        return model;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setDataModel(Model<T> model) {
        if (model == null) {
            throw new IllegalArgumentException("Cannot set a null Model");
        }
        if (this.model != null) {
            this.model.removeView(this);
        }
        model.addView(this);
        this.model = model;
        updateView();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public SelectModel<T> getSelectModel() {
        return new SelectModelImpl();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Filter<T> getFilter() {
        return flist.getFilter();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setFilter(Filter<T> filter) {
        flist.setFilter(filter);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setSorter(Comparator<T> comp) {
        flist.setSorter(comp);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Comparator<T> getSorter() {
        return flist.getSorter();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<T> getVisibleItems() {
        return flist;
    }

    public void setResourceBundle(ResourceBundle res) {
        this.res = res;
        for (ReflectionData ref : reflectionDataList) {
            int i = reflectionDataList.indexOf(ref);
            TableColumn tc = getColumnModel().getColumn(i);
            tc.setHeaderValue(getModel().getColumnName(i));


        }
    }

    List<ReflectionData> getReflectionDataList() {
        return reflectionDataList;
    }

    @Override
    public void setModel(final TableModel dataModel) {
        super.setModel(dataModel);
        setRowSorter(new TableRowSorter<>(dataModel));

        try {
            getDataModel().addListDataListener(new ListDataListener() {

                @Override
                public void intervalAdded(ListDataEvent e) {
                    try {
                        ((AbstractTableModel) dataModel).fireTableRowsInserted(flist.toViewIndex(e.getIndex0()), flist.toViewIndex(e.getIndex1()));
                    } catch (Exception excep) {
                    }
                }

                @Override
                public void intervalRemoved(ListDataEvent e) {
                    try {
                        getRowSorter().modelStructureChanged();
                        ((AbstractTableModel) dataModel).fireTableRowsDeleted(flist.toViewIndex(e.getIndex0()), flist.toViewIndex(e.getIndex1()));
                    } catch (Exception excep) {
                    }
                }

                @Override
                public void contentsChanged(ListDataEvent e) {
                    try {
                        ((AbstractTableModel) dataModel).fireTableRowsUpdated(flist.toViewIndex(e.getIndex0()), flist.toViewIndex(e.getIndex1()));
                    } catch (Exception excep) {
                    }
                }
            });
        } catch (Exception e) {
        }
    }

    private void initModel(Model<T> m) {
        setDataModel(m);

        setModel(new AbstractTableModel() {

            @Override
            public boolean isCellEditable(int row, int column) {
                return reflectionDataList.get(column).isEditable();
            }

            @Override
            public int getRowCount() {
                return flist.size();
            }

            @Override
            public int getColumnCount() {
                return reflectionDataList.size();
            }

            @Override
            public Object getValueAt(int rowIndex, int columnIndex) {
                try {
                    return reflectionDataList.get(columnIndex).get(flist.get(rowIndex));
                } catch (Exception ex) {
                    return null;
                }
            }

            @Override
            public void setValueAt(Object aValue, int rowIndex, int columnIndex) {

                ReflectionData ref = reflectionDataList.get(columnIndex);
                T obj = flist.get(rowIndex);

                try {
                    ref.set(obj, aValue);
                    if (getDataModel() instanceof DefaultModel
                            || DefaultModel.class.equals(getDataModel().getClass().getSuperclass())) {
                        ((DefaultModel<T>) getDataModel()).fireChanged(obj, flist.toDataModelIndex(rowIndex));
                    }
                    getDataModel().updateViews();
                    fireTableRowsUpdated(rowIndex, rowIndex);
                } catch (Exception ex) {
                    setEditingColumn(columnIndex);
                    setEditingRow(rowIndex);
                }
            }

            @Override
            public String getColumnName(int column) {
                String ID = reflectionDataList.get(column).getName();
                if (res != null && res.containsKey(ID)) {
                    return res.getString(ID);
                } else {
                    return ID;
                }
            }

            @Override
            public Class getColumnClass(int c) {
                try {
                    return getValueAt(0, c).getClass();
                } catch (Exception excep) {
                    return reflectionDataList.get(c).getType();
                }
            }
        });

    }

    private class SelectModelImpl implements SelectModel<T> {

        @Override
        public boolean isSelected(int index) {
            rangeCheck(index);
            if (getRowSorter() != null) {
                return getSelectionModel().isSelectedIndex(getRowSorter().convertRowIndexToView(index));
            }
            return getSelectionModel().isSelectedIndex(index);
        }

        @Override
        public boolean isSelected(T element) {
            return isSelected(getVisibleItems().indexOf(element));
        }

        @Override
        public void setSelected(int index, boolean select) {
            rangeCheck(index);
            try {
                if (getRowSorter() != null) {
                    index = getRowSorter().convertRowIndexToView(index);
                }
                if (select) {
                    getSelectionModel().addSelectionInterval(index, index);
                } else {
                    getSelectionModel().removeSelectionInterval(index, index);
                }
            } catch (Exception e) {
            }
        }

        @Override
        public void setSelected(T element, boolean select) {
            setSelected(getVisibleItems().indexOf(element), select);
        }

        @Override
        public void setAllSelected(boolean select) {
            if (getSelectionModel().getSelectionMode() == SelectionMode.SINGLE) {
                return;
            } else if (select) {
                getSelectionModel().setSelectionInterval(0, getVisibleItems().size() - 1);
            } else {
                getSelectionModel().clearSelection();
            }
        }

        @Override
        public List<T> getSelected() {
            List<T> result = new ArrayList<>();
            for (T e : getVisibleItems()) {
                if (isSelected(e)) {
                    result.add(e);
                }
            }
            return result;
        }

        @Override
        public int countSelected() {
            return getSelected().size();
        }

        @Override
        public void setMode(int mode) {
            getSelectionModel().setSelectionMode(mode);
        }

        @Override
        public int getMode() {
            return getSelectionModel().getSelectionMode();
        }

        private void rangeCheck(int index) {
            if (index >= getVisibleItems().size()) {
                throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + getVisibleItems().size());
            }
        }
    }
}

class ReflectionDataList extends ArrayList<ReflectionData> {

    private Set<String> names = new HashSet<>();
    private Class<?> clazz;

    @SuppressWarnings("OverridableMethodCallInConstructor")
    public ReflectionDataList(Class<?> c) {

        // reflection scanner
        clazz = c;
        Queue<ReflectionData> queue = new LinkedList<>();

        for (Field f : c.getDeclaredFields()) {
            if (f.isAnnotationPresent(Column.class)) {
                f.setAccessible(true);
                Column cn = f.getAnnotation(Column.class);
                FieldData fieldData = new FieldData(f, cn);
                names.add(fieldData.getName());
                queue.offer(fieldData);

            }
        }

        Method[] methods = c.getDeclaredMethods();
        for (Method m : methods) {

            if (m.isAnnotationPresent(Column.class)) {
                checkGetterMethod(m);
                m.setAccessible(true);
                Column cn = m.getAnnotation(Column.class);
                ReflectionData methodData = new MethodData(m, cn);
                names.add(methodData.getName());
                for (Method s : methods) {
                    if (isSetter(s, m)) {
                        methodData = new SetterData((MethodData) methodData, s);
                        break;
                    }
                }
                queue.offer(methodData);
            }
        }

        // arrange and indsert
        int maxCount = 1, count = 0;
        for (int i = 0; i < queue.size(); i++) {
            maxCount += i;
        }

        while (!queue.isEmpty()) {
            ReflectionData r = queue.poll();
            if (count++ > maxCount) {
                throw new RuntimeException("Order of columns in '" + clazz.getName() + "' results in cycle");
            }
            if (!add(r)) {
                queue.offer(r);
            }
        }
    }

    private void checkGetterMethod(Method m) throws RuntimeException, IllegalArgumentException {
        if (m.getParameterTypes().length > 0) {
            throw new IllegalArgumentException("The method '" + m.getName() + "' in '"
                    + clazz.getName() + "' must have no parameters.");
        }
        if (m.getReturnType() == void.class) {
            throw new RuntimeException("The return type of the method '"
                    + m.getName() + "' in '" + clazz.getName() + "' can not be void.");
        }

        if (!m.getName().startsWith("get") && !isBooleanGetter(m)) {
            throw new RuntimeException("The method '"
                    + m.getName() + "' in '" + clazz.getName() + "' is not well formed Getter.");
        }
    }

    private boolean isBooleanGetter(Method m) {
        return (m.getReturnType() == boolean.class || m.getReturnType() == Boolean.class) && m.getName().startsWith("is");
    }

    private String getSetterMethodName(Method m) {

        String name = m.getName();
        if (isBooleanGetter(m)) {
            return "set" + name.replaceFirst("is", "");
        } else if (name.startsWith("get")) {
            return "set" + name.replaceFirst("get", "");
        } else {
            return null;
        }

    }

    private boolean isSetter(Method setter, Method getter) {
        if (setter.getParameterTypes().length != 1) {
            return false;
        }
        if (!setter.getParameterTypes()[0].equals(getter.getReturnType())) {
            return false;
        }
        String setterMethodName = getSetterMethodName(getter);
        return setterMethodName != null && setterMethodName.equals(setter.getName());
    }

    private boolean exists(String name) {
        return names.contains(name);
    }

    @Override
    public boolean add(ReflectionData rd) {
        String before = rd.getAnnotation().beforecolumn();
        if (!before.isEmpty() && !rd.getName().equals(before)) {

            if (!exists(before)) {
                throw new NoSuchElementException("Column '" + before + "' in '" + clazz.getName() + "' doesn't exist");
            }

            for (ReflectionData r : this) {
                if (before.equals(r.getName())) {
                    add(indexOf(r), rd);
                    return true;
                }
            }
            return false;
        }

        if (rd.getAccessibleObject().isAnnotationPresent(Parent.class) || rd.getAccessibleObject().isAnnotationPresent(Children.class)) {
            add(0, rd);
            return true;
        } else {
            return super.add(rd);
        }
    }
}

abstract class ReflectionData {

    private Column annotation;

    public ReflectionData(Column annotation) {
        this.annotation = annotation;
    }

    public Column getAnnotation() {
        return annotation;
    }

    public String getName() {
        if (annotation.value().isEmpty()) {
            return getDefaultName();
        } else {
            return annotation.value();
        }
    }

    abstract AccessibleObject getAccessibleObject();

    abstract String getDefaultName();

    abstract boolean isEditable();

    abstract Class<?> getType();

    abstract Object get(Object obj) throws Exception;

    abstract void set(Object obj1, Object obj2) throws Exception;
}

class MethodData extends ReflectionData {

    private Method method;

    public MethodData(Method method, Column anno) {
        super(anno);
        this.method = method;

    }

    @Override
    Object get(Object obj) throws Exception {
        return method.invoke(obj);
    }

    @Override
    void set(Object obj1, Object obj2) throws Exception {
        throw new UnsupportedOperationException("Not supported.");
    }

    @Override
    Class<?> getType() {
        return method.getReturnType();
    }

    @Override
    boolean isEditable() {
        return false;
    }

    @Override
    String getDefaultName() {
        return method.getName().replaceFirst("is|get", "");
    }

    @Override
    AccessibleObject getAccessibleObject() {
        return method;
    }
}

class FieldData extends ReflectionData {

    private Field field;

    public FieldData(Field field, Column anno) {
        super(anno);
        this.field = field;
    }

    @Override
    Object get(Object obj) throws Exception {
        return field.get(obj);
    }

    @Override
    void set(Object obj, Object value) throws Exception {
        field.set(obj, value);
    }

    @Override
    Class<?> getType() {
        return field.getType();
    }

    @Override
    boolean isEditable() {
        return getAnnotation().editable();
    }

    @Override
    String getDefaultName() {
        return field.getName();
    }

    @Override
    AccessibleObject getAccessibleObject() {
        return field;
    }
}

class SetterData extends ReflectionData {

    private MethodData md;
    private Method method;

    public SetterData(MethodData md, Method m) {
        super(md.getAnnotation());
        this.md = md;
        this.method = m;
    }

    @Override
    boolean isEditable() {
        return getAnnotation().editable();
    }

    @Override
    Class<?> getType() {
        return md.getType();
    }

    @Override
    Object get(Object obj) throws Exception {
        return md.get(obj);
    }

    @Override
    void set(Object obj1, Object obj2) throws Exception {
        method.invoke(obj1, obj2);
    }

    @Override
    String getDefaultName() {
        return md.getDefaultName();
    }

    @Override
    AccessibleObject getAccessibleObject() {
        return md.getAccessibleObject();
    }
}
